# -*- coding: utf-8 -*-
# -- This file is part of the Apio project
# -- (C) 2016-2024 FPGAwars
# -- Authors
# --  * Jesús Arroyo (2016-2019)
# --  * Juan Gonzalez (obijuan) (2019-2024)
# -- License GPLv2
"""Implementation of 'apio api' command"""

import sys
import os
from typing import Dict, List, Self, Optional
from dataclasses import dataclass
import json
from pathlib import Path
import click
from apio.commands import options

# from apio.managers import packages
from apio.managers.examples import Examples, ExampleInfo
from apio.common.apio_console import cout, cerror
from apio.common.apio_styles import INFO
from apio.common.common_util import get_project_source_files
from apio.utils import cmd_util, usb_util, serial_util, util
from apio.utils.usb_util import UsbDevice
from apio.utils.serial_util import SerialDevice
from apio.apio_context import (
    ApioContext,
    PackagesPolicy,
    ProjectPolicy,
    RemoteConfigPolicy,
)
from apio.utils.cmd_util import (
    ApioGroup,
    ApioSubgroup,
    ApioCommand,
    ApioCmdContext,
)


timestamp_option = click.option(
    "timestamp",  # Var name.
    "-t",
    "--timestamp",
    type=str,
    metavar="text",
    help="Set a user provided timestamp.",
    cls=cmd_util.ApioOption,
)

output_option = click.option(
    "output",  # Var name.
    "-o",
    "--output",
    type=str,
    metavar="file-name",
    help="Set output file.",
    cls=cmd_util.ApioOption,
)


def write_as_json_doc(top_dict: Dict, output_flag: str, force_flag: bool):
    """A common function to write a dict as a JSON doc."""
    # -- Format the top dict as json text.
    text = json.dumps(top_dict, indent=2)

    if output_flag:
        # -- Output the json text to a user specified file.
        output_path = Path(output_flag)

        if output_path.is_dir():
            cerror(f"The output path {output_path} is a directory.")
            sys.exit(1)

        if output_path.exists() and not force_flag:
            cerror(f"The file already exists {output_path}.")
            cout("Use the --force option to allow overwriting.", style=INFO)
            sys.exit(1)

        # -- if there file path contains a parent dir, make
        # -- sure it exists. If output_flag is just a file name such
        # -- as 'foo.json', we don nothing.
        dirname = os.path.dirname(output_flag)
        if dirname:
            os.makedirs(dirname, exist_ok=True)

        # -- Write to file.
        with open(output_flag, "w", encoding="utf-8") as f:
            f.write(text)
    else:
        # -- Output the json text to stdout.
        print(text, file=sys.stdout)


# ------ apio api get-system


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_SYSTEM_HELP = """
The command 'apio api get-system' exports information about apio and \
the underlying system as a JSON foc.  It is similar to the command \
'apio info system' which is intended for human consumption.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-system               # Write to stdout
  apio api get-system -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-system",
    cls=ApioCommand,
    short_help="Retrieve apio and system information.",
    help=APIO_API_GET_SYSTEM_HELP,
)
# @click.pass_context
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_system_cli(
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-system' command."""

    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    section_dict = {}

    # -- Add fields.
    section_dict["apio-version"] = util.get_apio_version_str()
    section_dict["python-version"] = util.get_python_version()
    section_dict["platform-id"] = apio_ctx.platform_id
    section_dict["apio-python_package"] = str(
        util.get_path_in_apio_package("")
    )
    section_dict["apio-home-dir"] = str(apio_ctx.apio_home_dir)
    section_dict["apio-packages-dir"] = str(apio_ctx.apio_packages_dir)
    section_dict["remote-config-url"] = apio_ctx.profile.remote_config_url
    section_dict["verible-formatter"] = str(
        apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-format"
    )
    section_dict["verible-language-server"] = str(
        apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-ls"
    )

    # -- Add section
    top_dict["system"] = section_dict

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api get-project


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_PROJECT_HELP = """
The command 'apio api get-project' exports information about an Apio
project as a JSON foc.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-project               # Report default env
  apio api get-project -e env1       # Report specified env
  apio api get-project -p foo/bar    # Project in another dir
  apio api get-project -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-project",
    cls=ApioCommand,
    short_help="Get project information.",
    help=APIO_API_GET_PROJECT_HELP,
)
# @click.pass_context
@options.env_option_gen()
@options.project_dir_option
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_project_cli(
    # Options
    env: str,
    project_dir: Optional[Path],
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-project' command."""

    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.PROJECT_REQUIRED,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
        project_dir_arg=project_dir,
        env_arg=env,
    )

    # -- Change to the project's folder.
    os.chdir(apio_ctx.project_dir)

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    section_dict = {}

    active_env_dict = {}
    active_env_dict["name"] = apio_ctx.project.env_name
    active_env_dict["options"] = apio_ctx.project.env_options
    section_dict["active-env"] = active_env_dict

    section_dict["envs"] = apio_ctx.project.env_names

    synth_srcs, test_srcs = get_project_source_files()
    section_dict["synth-files"] = synth_srcs
    section_dict["test-benches"] = test_srcs

    # -- Add section
    top_dict["project"] = section_dict

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api get-boards


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_BOARDS_HELP = """
The command 'apio api get-boards' exports apio boards information as a \
JSON document.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-boards               # Write to stdout
  apio api get-boards -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-boards",
    cls=ApioCommand,
    short_help="Retrieve boards information.",
    help=APIO_API_GET_BOARDS_HELP,
)
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_boards_cli(
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-boards' command."""

    # -- For now, the information is not in a project context. That may
    # -- change in the future.
    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    # -- Generate the boards section.
    section = {}
    for board_id, board_info in apio_ctx.boards.items():
        # -- The board output dict.
        board_dict = {}

        # -- Add board description
        board_dict["description"] = board_info.get("description", None)

        # -- Add board's fpga information.
        fpga_dict = {}
        fpga_id = board_info.get("fpga-id", None)
        fpga_info = apio_ctx.fpgas.get(fpga_id, {})
        fpga_dict["id"] = fpga_id
        fpga_dict["part-num"] = fpga_info.get("part-num", None)
        fpga_dict["arch"] = fpga_info.get("arch", None)
        fpga_dict["size"] = fpga_info.get("size", None)
        board_dict["fpga"] = fpga_dict

        # -- Add board's programmer information.
        programmer_dict = {}
        programmer_id = board_info.get("programmer", {}).get("id", None)
        programmer_dict["id"] = programmer_id
        board_dict["programmer"] = programmer_dict

        # -- Add the board to the boards dict.
        section[board_id] = board_dict

    top_dict["boards"] = section

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api get-fpgas


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_FPGAS_HELP = """
The command 'apio api get-fpgas' exports apio FPGAss information as a \
JSON document.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-fpgas               # Write to stdout
  apio api get-fpgas -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-fpgas",
    cls=ApioCommand,
    short_help="Retrieve FPGAs information.",
    help=APIO_API_GET_FPGAS_HELP,
)
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_fpgas_cli(
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-fpgas' command."""

    # -- For now, the information is not in a project context. That may
    # -- change in the future.
    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    # -- Generate the fpgas section
    section = {}
    for fpga_id, fpga_info in apio_ctx.fpgas.items():
        # -- The fpga output dict.
        fpga_dict = {}

        fpga_dict["part-num"] = fpga_info.get("part-num", None)
        fpga_dict["arch"] = fpga_info.get("arch", None)
        fpga_dict["size"] = fpga_info.get("size", None)

        # -- Add the fpga to the fpgas dict.
        section[fpga_id] = fpga_dict

    top_dict["fpgas"] = section

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api get-programmers


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_PROGRAMMERS_HELP = """
The command 'apio api get-programmers' exports apio programmers information \
as a JSON document.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-programmers               # Write to stdout
  apio api get-programmers -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-programmers",
    cls=ApioCommand,
    short_help="Retrieve programmers information.",
    help=APIO_API_GET_PROGRAMMERS_HELP,
)
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_programmers_cli(
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-programmers' command."""

    # -- For now, the information is not in a project context. That may
    # -- change in the future.
    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    # -- Generate the 'programmers' section.
    section = {}
    for programmer_id, programmer_info in apio_ctx.programmers.items():
        section[programmer_id] = programmer_info

    top_dict["programmers"] = section

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api get-examples


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_EXAMPLES_HELP = """
The command 'apio api get-examples' exports apio examples information as a \
JSON document.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-examples               # Write to stdout
  apio api get-examples -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-examples",
    cls=ApioCommand,
    short_help="Retrieve examples information.",
    help=APIO_API_GET_EXAMPLES_HELP,
)
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_examples_cli(
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-examples' command."""

    # -- For now, the information is not in a project context. That may
    # -- change in the future.
    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- Get examples infos.
    examples: List[ExampleInfo] = Examples(apio_ctx).get_examples_infos()

    # -- Group examples by boards
    boards_examples: Dict[str, List[ExampleInfo]] = {}
    for example in examples:
        board_examples = boards_examples.get(example.board_id, [])
        board_examples.append(example)
        boards_examples[example.board_id] = board_examples

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    # -- Generate the 'examples' section.
    section = {}
    for board, board_examples in boards_examples.items():
        board_dict = {}
        # -- Generate board examples
        for example_info in board_examples:
            example_dict = {}
            example_dict["description"] = example_info.description
            board_dict[example_info.example_name] = example_dict

        section[board] = board_dict

    top_dict["examples"] = section

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api get-commands


@dataclass(frozen=True)
class CmdInfo:
    """Represents the information of a single apio command."""

    name: str
    path: List[str]
    cli: click.Command
    children: List[Self]


def scan_children(cmd_cli) -> Dict:
    """Return a dict describing this command subtree."""
    result = {}

    # -- Sanity check
    assert isinstance(result, dict), type(result)

    # -- If this is a simple command, it has no sub commands.
    if isinstance(cmd_cli, ApioCommand):
        return result

    # -- Here we have a group and it should have at least one sub command.
    assert isinstance(cmd_cli, ApioGroup), type(cmd_cli)
    subgroups: List[ApioSubgroup] = cmd_cli.subgroups

    # -- Create the dict for the command subgroups.
    subcommands_dict = {}
    result["commands"] = subcommands_dict

    # -- Iterate the subgroups and populate them. We flaten the subcommands
    # -- group into a single list of commands.
    for subgroup in subgroups:
        assert isinstance(subgroup, ApioSubgroup), type(subgroup)
        assert isinstance(subgroup.title, str), type(subgroup.title)
        for subcommand in subgroup.commands:
            subcommand_dict = scan_children(subcommand)
            subcommands_dict[subcommand.name] = subcommand_dict

    # -- All done ok.
    return result


# -- Text in the rich-text format of the python rich library.
APIO_API_GET_COMMANDS_HELP = """
The command 'apio api get-commands' exports apio command structure \
of Apio as a JSON doc. This is used by various tools such as
documentation generators and tests.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api get-commands               # Write to stdout
  apio api get-commands -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="get-commands",
    cls=ApioCommand,
    short_help="Retrieve apio commands information.",
    help=APIO_API_GET_COMMANDS_HELP,
)
@click.pass_context
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _get_commands_cli(
    # Click context
    cmd_ctx: ApioCmdContext,
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio get-commands' command."""

    # -- Find the top cli which is the "apio" command. Would access it
    # -- directly but it would create a circular python import.
    ctx = cmd_ctx
    while ctx.parent:
        ctx = ctx.parent
        assert isinstance(ctx, ApioCmdContext), type(ctx)
    top_cli = ctx.command
    assert top_cli.name == "apio", top_cli

    # -- This initializes the console, print active env vars, etc.
    ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    section_dict = {}
    section_dict["apio"] = scan_children(top_cli)
    top_dict["commands"] = section_dict

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio api scan-devices


# -- Text in the rich-text format of the python rich library.
APIO_API_SCAN_DEVICES_HELP = """
The command 'apio api scan-devices' scans and report the available usb and \
serial devices.

The optional flag '--timestamp' allows the caller to embed in the JSON \
document a known timestamp that allows to verify that the JSON document \
was indeed was generated by the same invocation.

Examples:[code]
  apio api scan-devices               # Write to stdout
  apio api scan-devices -o apio.json  # Write to a file[/code]
"""


@click.command(
    name="scan-devices",
    cls=ApioCommand,
    short_help="Scan and report available devices.",
    help=APIO_API_SCAN_DEVICES_HELP,
)
@timestamp_option
@output_option
@options.force_option_gen(short_help="Overwrite output file.")
def _scan_devices_cli(
    # Options
    timestamp: str,
    output: str,
    force: bool,
):
    """Implements the 'apio apio scan-devices' command."""

    # -- For now, the information is not in a project context. That may
    # -- change in the future. We need the config since we use libusb from
    # -- the packages.
    apio_ctx = ApioContext(
        project_policy=ProjectPolicy.NO_PROJECT,
        remote_config_policy=RemoteConfigPolicy.CACHED_OK,
        packages_policy=PackagesPolicy.ENSURE_PACKAGES,
    )

    # -- The top dict that we will emit as json.
    top_dict = {}

    # -- Append user timestamp if specified.
    if timestamp:
        top_dict["timestamp"] = timestamp

    # -- We need the packages for the 'libusb' backend.
    # packages.install_missing_packages_on_the_fly(apio_ctx.packages_context)

    usb_devices: List[UsbDevice] = usb_util.scan_usb_devices(apio_ctx)

    # -- Scan and report usb devices.
    section = []
    for device in usb_devices:
        dev = {}
        dev["vid"] = device.vendor_id
        dev["pid"] = device.product_id
        dev["bus"] = device.bus
        dev["device"] = device.device
        dev["manufacturer"] = device.manufacturer
        dev["product"] = device.product
        dev["serial-number"] = device.serial_number
        dev["device_type"] = device.device_type

        section.append(dev)

    top_dict["usb-devices"] = section

    # -- Scan and report serial devices.
    serial_devices: List[SerialDevice] = serial_util.scan_serial_devices()

    section = []
    for device in serial_devices:
        dev = {}
        dev["port"] = device.port
        dev["port-name"] = device.port_name
        dev["vendor-id"] = device.vendor_id
        dev["product-id"] = device.product_id
        dev["manufacturer"] = device.manufacturer
        dev["product"] = device.product
        dev["serial-number"] = device.serial_number
        dev["device-type"] = device.device_type

        section.append(dev)

    top_dict["serial-devices"] = section

    # -- Write out
    write_as_json_doc(top_dict, output, force)


# ------ apio apio

# -- Text in the rich-text format of the python rich library.
APIO_API_HELP = """
The command group 'apio api' contains subcommands that that are intended \
to be used by tools and programs such as icestudio, rather than being used \
directly by users.
"""

# -- We have only a single group with the title 'Subcommands'.
SUBGROUPS = [
    ApioSubgroup(
        "Subcommands",
        [
            _get_system_cli,
            _get_project_cli,
            _get_boards_cli,
            _get_fpgas_cli,
            _get_programmers_cli,
            _get_examples_cli,
            _get_commands_cli,
            _scan_devices_cli,
        ],
    )
]


@click.command(
    name="api",
    cls=ApioGroup,
    subgroups=SUBGROUPS,
    short_help="Apio programmatic interface.",
    help=APIO_API_HELP,
)
def cli():
    """Implements the 'apio apio' command group."""

    # pass
